package org.zhuzx.util.storage;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.prefs.Preferences;

import org.zhuzx.util.GeneralUtil;
import org.zhuzx.util.SelfCleaningCache;
import org.zhuzx.util.UnifiedConstants;

public class DataStorage {
	
	private static final Path dataPath = Paths.get("storage/data.sto");
	private static final Path indexPath = Paths.get("storage/index.sto");
	
	private static volatile byte[] data;
	private static Map<String,IndexUnit> index;
	private static final Map<String,Collection<String>> cache = new ConcurrentHashMap<>();
	static {
		initStorage();
	}
	
	/* 文件是否实现中间插入方式？似乎磁盘文件系统不支持真正的文件插入，性能不足取，需要调查。
	 * 完全随机存储，高并发，微小数据量：需要参考数据库的实现来决定存储文件的结构，暂缓。
	 * 批量存储，较大数据量：仍旧可按现有文件结构存储，需要改变容器结构，在初始化时将同集合下的分散于文件各处的数据收集起来组合成高效的复合结构。
	 * 比如一级结构为集合名以HashMap索引，二级则是数组以日期相关数字为下标，三级为时间索引数组，数组里面每个元素是单独的数据。
	 * 要根据数据类型的索引需要来决定其子结构，比如需要根据时间索引的数据可使用上述多级数组，可以用特定索引方式对数组快速定位。
	 * 尽量提供一次直接定位的机会，若不中则再使用二分查找等方法。对于许多数值型的索引值，直接以下标定位数组元素似乎是最佳选择。
	 * 本工具只适用于简单的数据，故先将问题简单化：1、只有单一索引需要；2、数据只用一个原子的字符串即可表达。
	 * 数据本身并不承担索引功能，所以和关系型数据库的表结构有根本性的不同。data-index的关系也和表-索引的关系不同，不能先入为主。
	 * 除了精确的查找需求，还需要有范围性的查找，若不能保证各批次数据的有序性，则可能需要以树、堆等特殊结构来作为子容器，因为数组只能用二分查找。
	 * 重新给data与index定位，原则上data应该承担更加无状态的、纯粹的信息，而index应该尽量精简，否则就违反了其存在的初衷。
	 */
	
	@Deprecated//加载数据文件时以枚举形式初始化所有集合名，乃至多层索引结构。
	private static void initStorage() {
		try {
			if (Files.exists(dataPath) && Files.exists(indexPath)) {
				data = Files.readAllBytes(dataPath);
				List<String> list = Files.readAllLines(indexPath, StandardCharsets.UTF_8);
				index = new ConcurrentHashMap<>(list.size());
				IndexUnit temp = null;
				for (String s : list) {
					String[] pair = s.split(":");
					temp = new IndexUnit(pair[1]);
					index.put(pair[0], temp);
				}
				if (temp!=null && temp.getEnd()!=data.length) {
					throw new RuntimeException("索引与数据内容不匹配！");
				}
			} else {
				if (!Files.exists(dataPath.getParent())) {
					Files.createDirectory(dataPath.getParent());
				}
				Files.deleteIfExists(dataPath);
				Files.deleteIfExists(indexPath);
				Files.createFile(dataPath);
				Files.createFile(indexPath);
				String head = "File Description:important data storage file"
						+ ", create at [" + UnifiedConstants.getStandardDaf().format(new Date()) + "]";
				Files.write(dataPath, head.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
				String headIndex = "File Description:0," + head.getBytes(StandardCharsets.UTF_8).length
						+ ",1," + DataContainer.HashSet.getContainerCode() + ",.,";
				Files.write(indexPath, headIndex.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
				data = Files.readAllBytes(dataPath);
				index = new ConcurrentHashMap<>();
			}
			System.out.println(">>>>init storage success, file info:\n" +
					"\tdata size: " + data.length + ", data file path: " + dataPath.toAbsolutePath().toString()
					+ "\n\tindex count: " + index.size());
		} catch (Exception e) {
			e.printStackTrace();
			System.out.println(">>>>init storage error");
		}
	}
	
	private static int writeData(String rangeKey, String specificData) {
		try {
			try (FileChannel channel = FileChannel.open(dataPath, StandardOpenOption.APPEND)) {
				specificData = "\n" + rangeKey + ":" + specificData;
				byte[] byteArray = specificData.getBytes(StandardCharsets.UTF_8);
				channel.write(ByteBuffer.wrap(byteArray));
				int newLength = (int) channel.size();
				return newLength;
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
	
	public static void writeSingleData(String key, String data) {
		write(key, data, null, DataContainer.SingleValue, 1);
	}
	
	public static String getSingleData(String key) {
		Collection<String> coll = getRangeData(key);
		if (!GeneralUtil.isEmpty(coll)) {
			return ((DataContainer.SingleValueCollection<String>) coll).getElement();
		}
		return null;
	}
	
	public static synchronized void write(String rangeKey, String specificData,
			String separator, DataContainer containerTpye, int recordCount) {
		int end = writeData(rangeKey, specificData);
		byte[] array = specificData.getBytes(StandardCharsets.UTF_8);
		int start = end - array.length;
		IndexUnit iu = new IndexUnit(start, end, recordCount, containerTpye.getContainerCode(), separator);
		String line = "\n" + rangeKey + ":" + iu.toString();
		try {
			Files.write(indexPath, line.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		
		byte[] newData = new byte[end];
		System.arraycopy(data, 0, newData, 0, data.length);
		System.arraycopy(array, 0, newData, start, array.length);
		data = newData;
		index.put(rangeKey, iu);
	}
	
	public static void write(String rangeKey, String specificData,
			String separator, DataContainer containerTpye) {
		int recordCount = new StringTokenizer(specificData, separator).countTokens();
		write(rangeKey, specificData, separator, containerTpye, recordCount);
	}
	
	@SuppressWarnings("unused")
	private static void saveFileModifiedTime() throws IOException {
		Preferences pref = Preferences.userNodeForPackage(DataStorage.class);
		pref.putLong(dataPath.toString(), Files.getLastModifiedTime(dataPath).toMillis());
		pref.putLong(indexPath.toString(), Files.getLastModifiedTime(indexPath).toMillis());
	}
	
	@SuppressWarnings("unused")
	private static boolean verifyFileIntegrity() {
		Preferences pref = Preferences.userNodeForPackage(DataStorage.class);
		try {
			return pref.getLong(dataPath.toString(),0) == Files.getLastModifiedTime(dataPath).toMillis() &&
					pref.getLong(indexPath.toString(),0) == Files.getLastModifiedTime(indexPath).toMillis();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return false;
	}
	
	private static Collection<String> getRangeData(String rangeKey) {
		Collection<String> coll = cache.get(rangeKey);
		if (coll == null) {
			IndexUnit iu = index.get(rangeKey);
			if (iu == null) {
				return null;
			}
			byte[] specificData = Arrays.copyOfRange(data, iu.getStart(), iu.getEnd());
			String[] array = splitByString(new String(specificData, StandardCharsets.UTF_8),
					iu.getSeparator(), iu.getRecordCount());
			DataContainer dc = DataContainer.getDataContainer(iu.getContainerType());
			coll = dc.getFilledContainer(array);
			cache.put(rangeKey, coll);
		}
		return coll;
	}
	
	public static boolean containsRangeKey(String rangeKey) {
		return cache.containsKey(rangeKey) || index.containsKey(rangeKey);
	}
	
	public static boolean contains(String value, String rangeKey) {
		Collection<String> coll = getRangeData(rangeKey);
		if (coll == null) return false;
		return coll.contains(value);
	}
	
	@SuppressWarnings("unchecked")
	@Deprecated//增加获取集合数据以及合并集合数据的副本（select）的公用方法，拆分本方法。
	public static boolean contains(String value, String[] rangeKeys) {
		if (rangeKeys.length == 1)
			return contains(value, rangeKeys[0]);
		String combinedKey = Arrays.toString(rangeKeys);
		Collection<String> allRangeData = (Collection<String>) SelfCleaningCache.get(combinedKey);
		if (allRangeData == null) {
			int type = -1;
			allRangeData = new HashSet<String>();
			for (String key : rangeKeys) {
				IndexUnit iu = index.get(key);
				if (iu == null)
					continue;
				if (type == -1) {
					type = iu.getContainerType();
					allRangeData = DataContainer.getDataContainer(type).getEmptyContainer();
				}
				if (type != iu.getContainerType())
					throw new RuntimeException("错误的索引键值！");
				allRangeData.addAll(getRangeData(key));
			}
			SelfCleaningCache.put(combinedKey, allRangeData);
		}
		return allRangeData.contains(value);
	}
	
	private static String[] splitByString(String str, String separator, int number) {
		if (GeneralUtil.isBlank(separator)) {
			if (number != 1) {
				throw new IllegalArgumentException("分隔符为空，但计数不为1：" + str);
			} else {
				return new String[] {str};
			}
		}
		String[] array = new String[number];
		StringTokenizer st = new StringTokenizer(str, separator);
		int i = 0;
		while (st.hasMoreTokens()) {
			array[i++] = st.nextToken();
		}
		return array;
	}
	
	private static class IndexUnit {
		private int start;
		private int end;
		private int recordCount;
		private int containerType;
		private String separator;
		@Deprecated//冒号本身作为数据分隔符的情况需要妥善处理。逗号本身更加复杂，无论在index文件中显示与否都会导致错位，是否转为char值？
		public IndexUnit(String info) {
			super();
			String[] array = info.split(",", -1);
			this.start = Integer.parseInt(array[0]);
			this.end = Integer.parseInt(array[1]);
			this.recordCount = Integer.parseInt(array[2]);
			this.containerType = Integer.parseInt(array[3]);
			this.separator = (array[4].length()==0) ? "," : array[4];
		}
		public IndexUnit(int start, int end, int recordCount, int containerType, String separator) {
			super();
			this.start = start;
			this.end = end;
			this.recordCount = recordCount;
			this.containerType = containerType;
			this.separator = separator;
		}
		public String toString() {
			return start + "," + end + "," + recordCount + "," + containerType + "," + separator;
		}
		public int getStart() {
			return start;
		}
		public int getEnd() {
			return end;
		}
		public int getRecordCount() {
			return recordCount;
		}
		public int getContainerType() {
			return containerType;
		}
		public String getSeparator() {
			return separator;
		}
	}
	
}